package tazadum.engine;

import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLProfile;
import javax.media.opengl.awt.GLCanvas;
import javax.swing.SwingUtilities;

import tazadum.engine.sound.Sound;
import tazadum.engine.ui.OpenGLFrame;

public class Engine {
	private final OpenGLFrame frame;
	private final Sound sound;

	private final Signal signal;
	private final TimeProvider timeProvider;
	private final float ratio;

	private final Set<Entity> entities = new HashSet<Entity>();
	private final TreeMap<Interval, Entity> activation = new TreeMap<Interval, Entity>();
	private final List<Interval> removeList = new ArrayList<Interval>(100);

	private final TreeMap<Float, Interval> displayList = new TreeMap<Float, Interval>();
	private final Map<Interval, Float> displayOrder = new HashMap<Interval, Float>();

	private boolean exit = false;
	private boolean precalced = false;
	private final GLCanvas canvas;
	
	public Engine(final String title, final int width, final int height, final boolean fullscreen) {
		this.timeProvider = new TimeProvider();
		this.signal = new Signal();
		this.sound = new Sound(timeProvider, signal);

		this.ratio = width / (float) height;

		System.setProperty("sun.java2d.noddraw", "true");

		this.frame = new OpenGLFrame(title, width, height);
		this.frame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(final WindowEvent e) {
				Engine.this.shutdown();
			}
		});

		final GLProfile profile = GLProfile.getDefault();
		final GLCapabilities caps = new GLCapabilities(profile);
		canvas = new GLCanvas(caps);

		canvas.addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(final KeyEvent e) {
				Engine.this.keyTyped(e);
			}
		});

		canvas.addGLEventListener(new GLEventListener() {
			@Override
			public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) {
				drawable.getGL().setSwapInterval(1);
				System.out.println(String.format("Reshape: (%d,%d)->(%d,%d)", x, y, width, height));
			}

			@Override
			public void init(final GLAutoDrawable drawable) {
				System.out.println("init");
				drawable.getGL().setSwapInterval(1);
				Engine.this.doPrecalc(drawable);
			}

			@Override
			public void dispose(final GLAutoDrawable drawable) {
				System.out.println("dispose");

				for (final Entity entity : entities) {
					entity.destroy(drawable);
				}
			}

			@Override
			public void display(final GLAutoDrawable drawable) {
				Engine.this.frameStart(drawable);
			}
		});

		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				Engine.this.frame.getContentPane().add(canvas);
				if (fullscreen) {
					Engine.this.frame.setFullscreen();
				}
				Engine.this.frame.setVisible(true);
			}
		});
	}

	public Signal getSignal() {
		return signal;
	}

	public void activate(final Entity entity, final Interval interval, final float zorder) {
		displayOrder.put(interval, zorder);
		entities.add(entity);
		activation.put(interval, entity);
	}

	protected void frameStart(final GLAutoDrawable drawable) {
		timeProvider.newFrame();
		final float dt = timeProvider.getFrameDeltaTime();
		final float time = timeProvider.getFrameTime();
		
		signal.generateBuiltInChannels(time, dt);

		// find which entities are active
		displayList.clear();
		removeList.clear();

		if (activation.isEmpty()) {
			System.out.println("End of demo");
			Engine.this.shutdown();
			return;
		}

		Interval key = activation.firstKey();
		while (key != null && key.isStarted(time)) {
			if (key.isAfter(time)) {
				removeList.add(key);
			} else {
				final Float order = displayOrder.get(key);
				displayList.put(order, key);
			}
			key = activation.higherKey(key);
		}

		// display the active entities
		for (final Interval interval : displayList.values()) {
			final Entity entity = activation.get(interval);
			entity.update(drawable, time, time - interval.from(), dt, interval);
		}

		// remove expired entities
		if (!removeList.isEmpty()) {
			for (final Interval interval : removeList) {
				activation.remove(interval);
			}
		}
	}

	protected void doPrecalc(final GLAutoDrawable drawable) {
		if (!precalced) {
			for (final Entity entity : entities) {
				entity.precalc(drawable, ratio);
			}
			precalced = true;
		}
	}

	protected void keyTyped(final KeyEvent e) {
		switch (e.getKeyCode()) {
		case KeyEvent.VK_ESCAPE:
			Engine.this.shutdown();
			break;
		case KeyEvent.VK_P:
			timeProvider.adjustTime(1);
			break;
		case KeyEvent.VK_F:
			System.out.println(String.format("FPS: %.2f", timeProvider.getFps()));
			break;
		}
	}

	public void start() {
		sound.initialize();
		sound.start();
		timeProvider.timeZero();
		while (!exit) {
			canvas.display();
			try {
				Thread.sleep(10);
			} catch (final InterruptedException e) {
				throw new RuntimeException(e);
			}
		}

		sound.stop();
		frame.shutdown();
		frame.dispose();
	}

	private void shutdown() {
		if (frame.isVisible()) {
			frame.setVisible(false);
		}

		exit = true;
	}

	public void fadeOutSound(float fadeOutTime, int fadeOutDuration) {
		sound.fadeOut(fadeOutTime, fadeOutDuration);
	}
}
